先前的章節已經看過的 map
函式,功能是將群集的各個元素套用到函式之中,產生新的群集。如果被套用的函式需要長時間的運算,等待所有元素都計算完畢就耗時過久。
pmap
函式 (Parallel map) 提供升級的 map
功能,將每個元素的運算分給不同的執行緒,所有元素計算完畢再彙整起來,如果有夠多的計算核心,完成的時間越縮短。
以下的範例中有一個模擬運行耗時十秒的運算,分別套用到有十個元素的群集,使用 pmap
比起 map
效能提升顯著:
(def data [2 4 6 8 10 12 14 16 18 20])
(defn long-computaion [n]
(Thread/sleep 10000)
(* n 2))
(time (dorun (map long-computaion data)))
;; => "Elapsed time: 100031.777566 msecs"
(time (dorun (pmap long-computaion data)))
;; => "Elapsed time: 10027.629015 msecs"
可以看到以上範例中,原來的 map
版本以約略於 100 秒的時間完成,而進化的 pmap
版本由於受益於並行化,以近似 10 秒的時間完成。
範例中使用 dorun
強制對 map
返回的惰性序列求值,並以 time
函式計算運行花費的時間。
另外還有 pvalues
以及 pcalls
分別並行地對多個運算式求值,以及呼叫多個函式:
(pvalues (+ 3 2) (/ 2 3) (* 3 2) (- 32 23))
;; => (5 2/3 6 9)
(pcalls #(println "A long time ago in a galaxy far,") #(println "far away") #(println "...."))
;; => A long time ago in a galaxy far,
;; => far away …
;; => (nil nil nil).
核心函式庫中的 map
、filter
、reduce
(它還有另外一個名字:fold
) 的作用是將一個群集轉換成另一個群集,雖然返回的是惰性序列仍然需要有創建的成本。
不同於核心函式庫的 reducer
函式庫,轉換的則不是資料結構,而是函式。不需要在一連串函式的轉換過程中創建暫時性的序列,而只是轉換運行的函式,此舉將會大大地增加效能。
clojure.core.reducer
函式中的 map
與 filter
函式並不回傳惰性序列,而是傳回屆時可以做化約 (reducible) 的函式,稱爲 reducer
。
其中使用了 Java 7 中用以執行並行任務的框架:Fork/Join
,將一連串的計算函式並行處理,減少處理時間。以下是典型的 map
與使用 reducer
後的各別效能評比:
(require '[clojure.core.reducers :as r])
(defn old-reduce [nums]
(reduce + (filter even? (map inc nums))))
(defn new-fold [nums]
(r/fold + (r/filter even? (r/map inc nums))))
(time (old-reduce (vec (range 1000000))))
;; => "Elapsed time: 136.409418 msecs"
;; => 250000500000
(time (new-fold (vec (range 1000000))))
;; => "Elapsed time: 96.708929 msecs"
;; => 250000500000
併發 (Concurrency) 是指同時有數個執行單元會交互執行,通常會牽涉一些共享的資源以及互相協作。
其實 Clojure 並沒有提供併發相關的函式或巨集,它提供了經過妥善設計的狀態管理方法,讓不同執行單元之間共享資源更容易管理且不易出錯。
在介紹狀態管理方法之前,先來了解 Clojure 對於事物的世界觀。
在 Clojure 世界中,一件事物分成身份 (Identity) 與狀態 (State),在時間的長河裡,每件事物在不同的時間有不同的狀態,狀態以值表示,由於值在 Clojure 世界中具有不變性,因此無法對值進行改變。如果要取得事物的狀態,則必須透過身份來取得,但是取得的狀態只是某個時間中狀態的快照 (Snapshot)。
例如一位名爲 Catherine 的使用者,20 歲時剛畢業開始工作,30 歲時結婚,不同的時間有不同的狀態,但是都是同一個身份。如果在傳統的程式語言,身份與狀態是含混不清的:
catherine.age = 20;
catherine.graduated = true;
;; 時間經過十年...
catherine.age += 10;
catherine.married = true;
上面的範例中,一個使用者類型既是代表某一種身份,更混雜了狀態。在 Clojure 中,狀態儲存在四種參考類型中,透過函式取得其中的狀態,新的狀態也是經由函式加上舊的狀態產生而成。而新的狀態在新的時間中,並不會影響其它時間的狀態。
若是有人在 20 秒前取得某個身份的狀態,10 秒後這個身份改變了狀態,之前取得的狀態並不會改變,保證了時間軸上狀態的一致。
Clojure 使用四種類型管理狀態,這些類型稱爲參考類型 (Reference type)。四種參考類型分別又隸屬於兩種分類:協作式 (Coordinated) 與同步式 (Synchronous),協作式指的是不同狀態更新時需要協調合作,同步式則是指在更新狀態前有可能會被阻攔或停滯,因爲其他部分正在更新,所以必須等待。
以下以圖形表示參考類型所屬的分類:
;; | Coordinated | Uncoordinated
;; ------|-------------|--------------
;; Sync | Ref | Atom
;; Async | N/A | Agent
(未完待續)